Наследование — механизм языка, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса. Класс–потомок может пользоваться родительскими полями, методами и свойствами, добавлять собственные поля, методы и свойства и переопределять методы родительских классов. Наследование позволяет строить иерархии классов. Наследование – возможность повторного использования кода. Наследуются поля, методы, свойства; поля, методы, свойства можно перекрывать в классах потомках. Перекрытые методы доступны в дочернем классе с помощью зарезервированного слова base.
В C# допустимо только одиночное наследование. То есть, непосредственно дочерний класс может наследовать только от одного родительского класса.
К основным типам связей (отношений) между классами предметной области относятся связи ассоциации, зависимости и обобщения.
Наличие отношения между классами свидетельствует о том, что объекты классов обмениваются сообщениями. Сообщение (message) — средство, с помощью которого объект-отправитель запрашивает у объекта-получателя выполнение одной из его операций.
В языке UML – унифицированном языке моделирования имеются соответствующие графические средства для описания различных видов связей между элементами объектной модели в частности между классами.
Ассоциация (association) — это семантическая связь между классами.
Ее изображают на диаграмме классов в виде обыкновенной линии (рисунок 1). Ассоциация отражает структурные связи между объектами различных классов.

Рисунок 1 - Ассоциация
Агрегация (aggregation) представляет собой форму ассоциации — более сильный тип связи между целым (составным) объектом и его частями (компонентными объектами).
Язык UML обеспечивает ограниченную поддержку агрегации. Сильная форма агрегации является в UML композицией. В композиции составной объект может физически содержать компонентные объекты. Компонентный объект может принадлежать только одному составному объекту.
Слабая форма агрегации в UML называется просто агрегацией. При этом составной объект физически не содержит компонентный объект. Один компонентный объект может обладать несколькими связями ассоциации или агрегации.
Агрегация изображается линией между классами с ромбом на стороне целого объекта (риунок. 2). Сплошной ромб представляет композицию (рисунок 3).

Рисунок 2 - Агрегация

Рисунок 3 - Композиция
Мощность (multiplicity) показывает, как много объектов участвует в связи.
Мощность — это число объектов одного класса, связанных с одним объектом другого класса. Понятие мощности связи в объектной модели аналогично понятиям мощности и класса принадлежности связи в модели «сущность-связь» (с точностью до расположения показателя мощности на диаграмме). Для каждой связи можно обозначить два показателя мощности — по одному на каждом конце связи.
В языке UML приняты следующие нотации для обозначения мощности (таблица 1).
Таблица 1 - Нотации для обозначения мощности
|
Значение мощности |
|
|
Мощность |
Значение |
|
* |
Много |
|
0 |
Нуль |
|
1 |
Один |
|
0..* |
Нуль или больше |
|
1..* |
Один или больше |
|
0..1 |
Нуль или один |
|
1..1 |
Ровно один |
Например, при разработке системы регистрации курсов в университете можно определить классы Дисциплина (учебная дисциплина) и Студент. Между ними установлена связь, означающая посещение дисциплин студентами. Если один студент может посещать от нуля до четырех дисциплин, а одну дисциплину могут посещать от 10 до 20 студентов, то на диаграмме классов это можно изобразить следующим образом (рисунок 4).

Рисунок 4 - Мощность связи
Зависимость (dependency) — связь между двумя элементами модели, при которой изменения в спецификации одного элемента могут повлечь за собой изменения в другом элементе.
Зависимость — слабая форма связи между клиентом и сервером (клиент зависит от сервера и не имеет знаний о сервере). Зависимость изображается пунктирной линией, направленной от клиента к серверу (рисунок 5).

Рисунок 5 -Зависимость
Зависимость между двумя элементами имеет место в том случае, если изменения в определении одного элемента могут повлечь за собой изменения в другом. Причины для зависимостей могут быть самыми разными: один класс посылает сообщение другому; один класс включает часть данных другого класса; один класс использует другой в качестве параметра операции. Если класс меняет свой интерфейс, то любое сообщение, которое ему посылают, может утратить свою силу.
Обобщение (generalization) — связь «тип-подтип» реализует механизм наследования (inheritance). Большинство объектно-ориентированных языков непосредственно поддерживают концепцию наследования. Она позволяет одному классу наследовать все атрибуты, операции и связи другого.
В языке UML связи наследования называют обобщениями и изображают в виде стрелок от класса-потомка к классу-предку (рисунок 6).

Рисунок 6 - Обобщение
Общие атрибуты, операции и/или связи отображаются на верхнем уровне иерархии.
Помимо наследуемых, каждый подкласс имеет свои собственные уникальные атрибуты, операции и связи.
Вторым принципом ООП является наследование. Этот простой принцип означает, что если вы хотите создать новый класс, лишь немного отличающийся от старого, то нет необходимости в переписывании заново уже существующих полей и методов. Вы объявляете, что новый класс является потомком или дочерним классом старого класса, называемого предком или родительским классом, и добавляете к нему новые поля, методы и свойства – иными словами, то, что необходимо при переходе от общего к частному.
В C# все классы являются потомками класса System.Object. Поэтому, если вы строите дочерний класс прямо от System.Object, то в определении его можно не упоминать. Следующие два выражения одинаково верны:
class MyClass
{
}
class MyClass:Object
{
}
Описание класса-потомка
Класс в C# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object:
[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]
тело класса
Примечание
Обратите внимание, что слово "предки" присутствует в описании класса во множественном числе, хотя класс может иметь только одного предка. Причина в том, что класс наряду с единственным предком может наследовать от интерфейсов — специального вида классов, не имеющих реализации.
Рассмотрим наследование классов на примере. Пусть у нас имеется класс Person (человек), описывающий отдельного человека:
class Person
{
private string _firstName;
private string _lastName;
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
public string LastName
{
get { return _lastName; }
set { _lastName = value; }
}
public void Display()
{
Console.WriteLine(FirstName + " " + LastName);
}
}
Но вдруг нам потребовался класс, описывающий сотрудника предприятия - класс Employee (Служащий). Поскольку этот класс будет использовать те же данные и методы, что и класс Person, так как сотрудник - это также и человек, то было бы рационально сделать класс Employee производным (или наследником) от класса Person, который, в свою очередь, называется базовым классом или родителем:
class Employee:Person
{
}
Примечание
Базовый класс или родитель, производный класс или дочерний класс.
После двоеточия мы указываем базовый класс для данного класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все свойства, методы, поля, которые есть в классе Person.
Таким образом, наследование реализует отношение is-a (является), объект класса Employee также является объектом класса Person:
class Program
{
static void Main(string[] args)
{
Person p = new Person { FirstName = "Bill", LastName = "Gates" };
p.Display();
p = new Employee { FirstName = "Denis", LastName = "Ritchi" };
p.Display();
Console.Read();
}
}
И поскольку объект Employee является также и объектом Person, то мы можем так определить переменную:
Person p = new Employee().
По умолчанию можно наследовать от любого класса. Однако здесь есть ряд ограничений:
• Не поддерживается множественное наследование, класс может наследоваться только от одного класса. Хотя проблема множественного наследования реализуется с помощью концепции интерфейсов, о которых мы поговорим позже.
• При создании производного класса надо учитывать тип доступа к базовому классу - тип доступа к производному классу должен быть таким же, как и у базового класса, или более строгим. То есть, если базовый класс у нас имеет тип доступа internal, то производный класс может иметь тип доступа internal или private, но не public.
• Если класс объявлен с модификатором sealed, то от этого класса нельзя наследовать и создавать производные классы. Например, следующий класс не допускает создание наследников:
sealed class Admin
{
}
Вернемся к нашим классам Person и Employee. Хотя Employee наследует поля, свойства и методы от класса Person, посмотрим, что будет в следующем случае:
class Employee:Person
{
public void Display()
{
Console.WriteLine(_firstName);
}
}
При написании этого кода мы получим сообщение об ошибке, так как переменная _firstName объявлена с модификатором private и поэтому к ней доступ имеет только класс Person. Но зато в классе Person определено общедоступное свойство FirstName, которое мы можем использовать, поэтому следующий код ошибку не вызовет:
class Employee:Person
{
public void Display()
{
Console.WriteLine(FirstName);
}
}
Таким образом, производный класс может иметь доступ только к тем членам базового класса, которые определены с модификаторами public, internal, protected и protected internal.
Ключевое слово base
Теперь добавим в наши классы конструкторы:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string fName, string lName)
{
FirstName = fName;
LastName = lName;
}
public void Display()
{
Console.WriteLine(FirstName + " " + LastName);
}
}
class Employee : Person
{
public string Company { get; set; }
public Employee(string fName, string lName, string comp) : base(fName, lName)
{
Company = comp;
}
}
Класс Person имеет конструктор, который устанавливает два свойства. Поскольку класс Employee наследует и устанавливает те же свойства, что и класс Person, то логично было бы не писать второй раз код установки, а как-то вызвать соответствующий код класса Person. К тому же свойств, которые надо установить, и параметров может быть гораздо больше.
С помощью ключевого слова base мы можем обратиться к базому классу. В нашем случае в конструкторе класса Employee нам надо установить имя, фамилию и компанию. Но имя и фамилию мы передаем для инициализации в конструктор базового класса, то есть в конструктор класса Person, с помощью выражения base(fName, lName).
class Program
{
static void Main(string[] args)
{
Person p = new Person("Bill", "Gates");
p.Display();
Employee emp = new Employee("Tom", "Simpson", "Microsoft");
emp.Display();
Console.Read();
}
}
После выполнения программы мы увидим (рисунок 7):

Рисунок 7 – Результат выполнения программы
Как видите, экземпляр класса Employee с одинаковой легкостью использует как собственные, так и унаследованные элементы класса. Рассмотрим общие правила наследования.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными ниже правилами.
• Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров.
• Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. Таким образом, каждый конструктор инициализирует свою часть объекта.
• Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации. Вызов выполняется с помощью ключевого слова base. Вызывается та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base.
Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new.
Из предыдущего примера видно, что для вывода свойства Company класса Employee нам необходимо изменить код метода Display. Если мы хотим в классе Employee определить метод под тем же именем, что и в классе Person, это можно сделать использую модификатор new:
class Employee : Person
{
public string Company { get; set; }
public Employee(string fName, string lName, string comp)
: base(fName, lName)
{
Company = comp;
}
new public void Display()
{
Console.WriteLine(FirstName + " " + LastName + " " + Company);
}
}
Метод Display класса Employee перекроет метод Display класса Person. При выполнении следующего кода
class Program
{
static void Main(string[] args)
{
Person p = new Person("Bill", "Gates");
p.Display();
Employee emp = new Employee("Tom", "Simpson", "Microsoft");
emp.Display();
Console.Read();
}
}
мы увидим (рисунок 8):

Рисунок 8 – Результат выполнения программы
При использовании в качестве модификатора ключевое слово new явно скрывает члены, унаследованные от базового класса. При сокрытии унаследованного члена его производная версия заменяет версию базового класса. Несмотря на то, что члены можно скрыть и без модификатора new, в результате возникнет предупреждение. Если new используется для явного скрытия члена, модификатор отключает вывод предупреждение и документирует тот факт, что производная версия предназначена для замены.
Чтобы скрыть унаследованный член, объявите его в производном классе с помощью такого же имени и измените с помощью модификатора new.
Сокрытие имени через наследование принимает одну из следующих форм.
• Константа, поле, свойство или тип, представленные в классе или структуре, скрывают все члены базового класса с одним именем.
• Метод, представленный в классе или структуре, скрывает в базовом классе свойства, поля и типы с тем же именем. Он также скрывает все методы базовых классов, имеющие одинаковую сигнатуру.
• Индексатор, представленный в классе или структуре, скрывает все индексаторы базового класса, имеющие одинаковую сигнатуру.
Однако мы можем использовать в производном классе перекрытый метод базового класса, используя ключевое слово base:
class Employee : Person
{
public string Company { get; set; }
public Employee(string fName, string lName, string comp)
: base(fName, lName)
{
Company = comp;
}
new public void Display()
{
base.Display();
Console.WriteLine("Место работы: " + Company);
}
}
При выполнении следующего кода
class Program
{
static void Main(string[] args)
{
Person p = new Person("Bill", "Gates");
p.Display();
Employee emp = new Employee("Tom", "Simpson", "Microsoft");
emp.Display();
Console.Read();
}
}
мы увидим (рисунок 9):

Рисунок 9 – Результат выполнения программы
Ключевое слово base используется для доступа к членам базового класса из производного класса:
• Вызов метода базового класса, который был переопределен другим методом.
• Определение конструктора базового класса, который должен вызываться при создании экземпляров производного класса.
Доступ к базовому классу разрешен только в конструкторе, методе экземпляра или методе доступа экземпляра.
Использование ключевого слова base в статическом методе является недопустимым.
Базовый класс, к которому осуществляется доступ, является базовым классом, заданным в объявлении класса. Например, если указать class ClassB : ClassA, члены ClassA будут доступны из ClassB, независимо от базового класса ClassA.
Во время выполнения программы объекты хранятся в отдельных переменных, массивах или других коллекциях. Во многих случаях удобно оперировать объектами одной иерархии единообразно, то есть использовать один и тот же программный код для работы с экземплярами разных классов. Это возможно благодаря тому, что объекту базового класса можно присвоить объект производного класса.
Попробуем описать массив объектов базового класса и занести туда объекты производного класса. В листинге 1 в массиве типа Person хранятся два объекта типа Person и один — типа Employee.
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string fName, string lName)
{
FirstName = fName;
LastName = lName;
}
public void Display()
{
Console.WriteLine(FirstName + " " + LastName);
}
}
class Employee : Person
{
public string Company { get; set; }
public Employee(string fName, string lName, string comp)
: base(fName, lName)
{
Company = comp;
}
new public void Display()
{
Console.WriteLine(FirstName + " " + LastName + "Место работы: " + Company);
}
}
class Program
{
static void Main(string[] args)
{
Person[] m = new Person[3];
m[0] = new Person("Иван", "Петров");
m[1] = new Person("Семён", "Иванов");
m[2] = new Employee("Степан", "Кузнецов","2ГИС");
foreach (Person p in m) p.Display();
}
}
Листинг 1. Массив объектов разных типов.
Результат работы программы (рисунок 10):

Рисунок 10 – Результат выполнения программы
Результат радует нас только частично: объект типа Employee действительно можно поместить в массив, состоящий из элементов типа Person, но для него вызываются только методы и свойства, унаследованные от предка.
Итак, присваивать объекту базового класса объект производного класса можно, но вызываются для него только методы и свойства, определенные в базовом классе. Иными словами, возможность доступа к элементам класса определяется типом ссылки, а не типом объекта, на который она указывает.
Это и понятно: ведь компилятор должен еще до выполнения программы решить, какой метод вызывать, и вставить в код фрагмент, передающий управление на этот метод (этот процесс называется ранним связыванием). При этом компилятор может руководствоваться только типом переменной, для которой вызывается метод или свойство. То, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компилятор учесть не может.
Следовательно, если мы хотим, чтобы вызываемые методы соответствовали типу объекта, необходимо отложить процесс связывания до этапа выполнения программы, а точнее — до момента вызова метода, когда уже точно известно, на объект какого типа указывает ссылка. Такой механизм в C# есть — он называется поздним связыванием и реализуется с помощью, так называемых виртуальных методов.
В C# множественное наследование отсутствует. Если вы хотите, чтобы новый класс объединял свойства нескольких, порождайте классы-потомки один от другого или включите в класс несколько полей, соответствующих этим желаемым классам. Например:
class TClass1
{
}
class TClass2:TClass1
{
}
//TClass3 наследует одновременно свойства класса TClass1 и TClass2
class TClass3:TClass2
{
}
//----------------------------
//или
//----------------------------
//TClass3 наследует одновременно свойства класса TClass1 и TClass2
class TClass3
{
TClass1 FObj1;
TClass2 FObj2;
}
Рассмотрим следующую иерархию классов (рисунок 11):

Рисунок 11 – Иерархия классов
Здесь T,T1,T2,T3,T11,T31 – идентификаторы классов. Тогда классы T1,T2,T3 для типа T будут дочерними классами, а класс T для них будет родительским классом. Классы T1,T2,T3,T11,T31 для класса T будут потомками, а класс T для них будет предком.
Совместимость по присваиванию для классов определена так: переменная типа класс может ссылаться на объект потомка. Это же правило действует при подстановке фактических параметров вместо формальных параметров при вызове методов. Если для приведённой выше иерархии классов описать переменные,
class T
{
}
class T1:T
{
}
class T2:T
{
}
class T3:T
{
}
class T11:T1
{
}
class T31:T3
{
}
то для следующих присваиваний можно сказать (рисунок 12):
namespace ConsoleInheretance5
{
class Program
{
static void Main(string[] args)
{
T O = new T();
T1 O1 = new T1();
T2 O2 = new T2();
T3 O3 = new T3();
T11 O11 = new T11();
T31 O31 = new T31();
O = O1;//допустимо
O = O2; //допустимо
O = O3; //допустимо
O1 = O11; //допустимо
O3 = O31; //допустимо
//O3 = O11; //не допустимо
//O11 = O31; //не допустимо
//O31 = O3; //не допустимо
Show(O3);
}
static void Show(T obj)
{
Console.WriteLine(obj.ToString());
}
}
}

Рисунок 12 – Результат выполнения программы
Таким образом, объект О является полиморфным. Потому что переменная О может указывать на объект класса Т, либо любого класса наследника класса Т.
1. Что такое наследование?
2. Что такое одиночное наследование?
3. Что такое множественное наследование?
4. Как реализовать множественное наследование в C#?
5. Что может наследовать один класс от другого?
6. Как обращаться к унаследованным полям, методам и свойствам?
7. Что такое перекрытие?
8. Что можно перекрывать в дочернем классе?
9. Можно ли получить доступ к перекрытым полям родителя?
10. Можно ли получить доступ к перекрытым методам родителя?
11. Как перекрываются статические методы?
12. Что такое раннее связывание?
13. Для каких методов выполняется раннее связывание?
14. Сформулируйте правило совместимости по присваиванию для объектовых типов?
15. Какие поля, методы и свойства родителя недоступны в методах дочернего класса?